//This file contains assistant functions called by methods of ModelContract.
package main

import (
	"encoding/json"
	"fmt"
	"github.com/hyperledger/fabric-contract-api-go/contractapi"
	"strconv"
	"time"
)

//UnknownTransactionHandler handles the problem that the function called by nodes doesn't match any method of ModelContract
func UnknownTransactionHandler(ctx contractapi.TransactionContextInterface) error {
	fcn, args := ctx.GetStub().GetFunctionAndParameters()
	return fmt.Errorf("invalid function %s passed with args %v", fcn, args)
}

//IterUpdater is called to keep Iter up-to-date whenever a node use ModelContract to query or submit.
//For example, if a new node join in the middle of federated learning process or a node reconnect to the process after down for a while,
//the node has Iter=1, while in fact the process has already reached 7th round, which means other normal nodes has Iter=7.
//In such case, IterUpdater will change Iter of the node to 7, keeping all nodes in sync.
func IterUpdater(ctx contractapi.TransactionContextInterface) error {
	defaultIteration:=0

	result,err:=ctx.GetStub().GetState("Iteration")
	if err != nil {
		return fmt.Errorf("cannot interact with the leadger:%s",err.Error())
	}
	if result != nil {
		searchedGM:=new(GlobalModel)
		err=json.Unmarshal(result,searchedGM)
		if err != nil {
			return fmt.Errorf("cannot unmarshal value into GlobalModel struction: %s",err.Error())
		}
		defaultIteration=searchedGM.Iteration+1
	}
	Iter=defaultIteration
	return nil
}

//LocalModelQueryer queries all new local models that was created after latest global model.
//It returns in the form of map.
func LocalModelQueryer(ctx contractapi.TransactionContextInterface, iteration string) (map[string][]byte,error) {

	resultsIterator,err:=ctx.GetStub().GetStateByPartialCompositeKey("ModelInformation",[]string{strconv.FormatBool(true),iteration})

	if err != nil {
		return nil, fmt.Errorf("cannot read %sth iteration localmodel because of searching failed",iteration)
	}
	defer func() {
		_ = resultsIterator.Close()
	}()

	KeyValuePairs:=make(map[string][]byte)
	for resultsIterator.HasNext() {
		queryResponse, err := resultsIterator.Next()
		if err != nil {
			return nil, fmt.Errorf("function Next error: %s",err.Error())
		}
		KeyValuePairs[queryResponse.Key]=queryResponse.Value
	}
	return KeyValuePairs, nil
}

//GlobalModelSubmitter is called to submit a global model of given iteration into ledger
func GlobalModelSubmitter(ctx contractapi.TransactionContextInterface, iteration int, producer string, value string) (string, error) {
	NowTime:=time.Now()
	mn:= ModelInformation{false,iteration,producer,NowTime.Format("2006-01-02 15:04:05")}
	gm:= GlobalModel{mn,value}
	gmAsBytes,err:=json.Marshal(gm)
	if err != nil {
		return "",fmt.Errorf("cannot marshal GlobalModel: %s",err.Error())
	}

	err = ctx.GetStub().PutState("Iteration", gmAsBytes)
	if err != nil {
		return "",fmt.Errorf("unable to interact with world state :%s",err.Error())
	}

	Info :=fmt.Sprintf("successfully put a global model to the ledger:maker %s,Iteration %d,created time %s",producer,Iter,NowTime.Format("2006-01-02 15:04:05"))

	return Info,nil
}

//Aggregator queries TrainingParameters of all new local models that was created after latest global model.
//Then it average these TrainingParameters into single TrainingParameters, and return the pointer.
//If there is no new local model, it returns nil pointer and nil error.
func Aggregator(ctx contractapi.TransactionContextInterface, iteration string) (*TrainingParameters,error){
	resultsIterator,err:=ctx.GetStub().GetStateByPartialCompositeKey("ModelInformation",[]string{strconv.FormatBool(true),iteration})
	if err != nil {
		return nil, fmt.Errorf("cannot read local models because of searching failed: %s",err.Error())
	}
	defer func() {
		_ = resultsIterator.Close()
	}()

	var results []*TrainingParameters
	if resultsIterator.HasNext() {
		for resultsIterator.HasNext() {
			queryResponse, err := resultsIterator.Next()
			if err != nil {
				return nil, fmt.Errorf("error while traverse resultsIterator: %s",err.Error())
			}
			result:=new(TrainingParameters)
			err=json.Unmarshal(queryResponse.Value,result)
			if err!=nil {
				return nil,fmt.Errorf("failed to unmarshal: %s",err)
			}
			results=append(results,result)
		}
	} else {
		return nil,nil
	}

	AverData,err:= AverageHandler(results)
	if err != nil {
		return nil,fmt.Errorf("function AverageHandler error: %s",err.Error())
	}

	return AverData,nil
}
